Utforska grunderna i binära sökträd (BST) och lär dig hur du implementerar dem effektivt i JavaScript. Denna guide täcker BST-struktur, operationer och praktiska exempel för utvecklare världen över.
Binära sökträd: En omfattande implementeringsguide i JavaScript
Binära sökträd (BST) är en fundamental datastruktur inom datavetenskap, som används flitigt för effektiv sökning, sortering och hämtning av data. Deras hierarkiska struktur möjliggör logaritmisk tidskomplexitet för många operationer, vilket gör dem till ett kraftfullt verktyg för att hantera stora datamängder. Denna guide ger en omfattande översikt över BST och demonstrerar deras implementering i JavaScript, riktad till utvecklare världen över.
Förståelse för binära sökträd
Vad är ett binärt sökträd?
Ett binärt sökträd är en trädbaserad datastruktur där varje nod har högst två barn, kallade vänster barn och höger barn. Den viktigaste egenskapen hos ett BST är att för en given nod gäller:
- Alla noder i det vänstra underträdet har nycklar som är mindre än nodens nyckel.
- Alla noder i det högra underträdet har nycklar som är större än nodens nyckel.
Denna egenskap säkerställer att elementen i ett BST alltid är ordnade, vilket möjliggör effektiv sökning och hämtning.
Nyckelbegrepp
- Nod: En grundläggande enhet i trädet, som innehåller en nyckel (datan) och pekare till dess vänstra och högra barn.
- Rot: Den översta noden i trädet.
- Löv: En nod utan barn.
- Underträd: En del av trädet med roten i en specifik nod.
- Höjd: Längden på den längsta vägen från roten till ett löv.
- Djup: Längden på vägen från roten till en specifik nod.
Implementering av ett binärt sökträd i JavaScript
Definiera Node-klassen
Först definierar vi en `Node`-klass för att representera varje nod i BST:t. Varje nod kommer att innehålla en `key` för att lagra datan och `left`- och `right`-pekare till sina barn.
class Node {
constructor(key) {
this.key = key;
this.left = null;
this.right = null;
}
}
Definiera BinarySearchTree-klassen
Därefter definierar vi `BinarySearchTree`-klassen. Denna klass kommer att innehålla rotnoden och metoder för att infoga, söka, ta bort och traversera trädet.
class BinarySearchTree {
constructor() {
this.root = null;
}
// Metoder kommer att läggas till här
}
Insättning
`insert`-metoden lägger till en ny nod med den givna nyckeln till BST:t. Insättningsprocessen bibehåller BST-egenskapen genom att placera den nya noden på lämplig position i förhållande till befintliga noder.
insert(key) {
const newNode = new Node(key);
if (this.root === null) {
this.root = newNode;
} else {
this.insertNode(this.root, newNode);
}
}
insertNode(node, newNode) {
if (newNode.key < node.key) {
if (node.left === null) {
node.left = newNode;
} else {
this.insertNode(node.left, newNode);
}
} else {
if (node.right === null) {
node.right = newNode;
} else {
this.insertNode(node.right, newNode);
}
}
}
Exempel: Infoga värden i BST:t
const bst = new BinarySearchTree();
bst.insert(11);
bst.insert(7);
bst.insert(15);
bst.insert(5);
bst.insert(3);
bst.insert(9);
bst.insert(8);
bst.insert(10);
bst.insert(13);
bst.insert(12);
bst.insert(14);
bst.insert(20);
bst.insert(18);
bst.insert(25);
Sökning
`search`-metoden kontrollerar om en nod med den givna nyckeln finns i BST:t. Den traverserar trädet, jämför nyckeln med den aktuella nodens nyckel och rör sig till vänster eller höger underträd därefter.
search(key) {
return this.searchNode(this.root, key);
}
searchNode(node, key) {
if (node === null) {
return false;
}
if (key < node.key) {
return this.searchNode(node.left, key);
} else if (key > node.key) {
return this.searchNode(node.right, key);
} else {
return true;
}
}
Exempel: Söka efter ett värde i BST:t
console.log(bst.search(9)); // Utskrift: true
console.log(bst.search(2)); // Utskrift: false
Borttagning
`remove`-metoden tar bort en nod med den givna nyckeln från BST:t. Detta är den mest komplexa operationen eftersom den måste bibehålla BST-egenskapen när noden tas bort. Det finns tre fall att beakta:
- Fall 1: Noden som ska tas bort är en lövnod. Ta helt enkelt bort den.
- Fall 2: Noden som ska tas bort har ett barn. Ersätt noden med dess barn.
- Fall 3: Noden som ska tas bort har två barn. Hitta in-order-efterföljaren (den minsta noden i det högra underträdet), ersätt noden med efterföljaren och ta sedan bort efterföljaren.
remove(key) {
this.root = this.removeNode(this.root, key);
}
removeNode(node, key) {
if (node === null) {
return null;
}
if (key < node.key) {
node.left = this.removeNode(node.left, key);
return node;
} else if (key > node.key) {
node.right = this.removeNode(node.right, key);
return node;
} else {
// nyckeln är lika med nodens nyckel
// fall 1 - en lövnod
if (node.left === null && node.right === null) {
node = null;
return node;
}
// fall 2 - noden har endast 1 barn
if (node.left === null) {
node = node.right;
return node;
} else if (node.right === null) {
node = node.left;
return node;
}
// fall 3 - noden har 2 barn
const aux = this.findMinNode(node.right);
node.key = aux.key;
node.right = this.removeNode(node.right, aux.key);
return node;
}
}
findMinNode(node) {
let current = node;
while (current != null && current.left != null) {
current = current.left;
}
return current;
}
Exempel: Ta bort ett värde från BST:t
bst.remove(7);
console.log(bst.search(7)); // Utskrift: false
Trädgenomgång
Trädgenomgång (traversering) innebär att besöka varje nod i trädet i en specifik ordning. Det finns flera vanliga genomgångsmetoder:
- In-order: Besöker vänster underträd, sedan noden, sedan höger underträd. Detta resulterar i att noderna besöks i stigande ordning.
- Pre-order: Besöker noden, sedan vänster underträd, sedan höger underträd.
- Post-order: Besöker vänster underträd, sedan höger underträd, sedan noden.
inOrderTraverse(callback) {
this.inOrderTraverseNode(this.root, callback);
}
inOrderTraverseNode(node, callback) {
if (node !== null) {
this.inOrderTraverseNode(node.left, callback);
callback(node.key);
this.inOrderTraverseNode(node.right, callback);
}
}
preOrderTraverse(callback) {
this.preOrderTraverseNode(this.root, callback);
}
preOrderTraverseNode(node, callback) {
if (node !== null) {
callback(node.key);
this.preOrderTraverseNode(node.left, callback);
this.preOrderTraverseNode(node.right, callback);
}
}
postOrderTraverse(callback) {
this.postOrderTraverseNode(this.root, callback);
}
postOrderTraverseNode(node, callback) {
if (node !== null) {
this.postOrderTraverseNode(node.left, callback);
this.postOrderTraverseNode(node.right, callback);
callback(node.key);
}
}
Exempel: Genomgång av BST:t
const printNode = (value) => console.log(value);
bst.inOrderTraverse(printNode); // Utskrift: 3 5 8 9 10 11 12 13 14 15 18 20 25
bst.preOrderTraverse(printNode); // Utskrift: 11 5 3 9 8 10 15 13 12 14 20 18 25
bst.postOrderTraverse(printNode); // Utskrift: 3 8 10 9 12 14 13 18 25 20 15 11
Minsta och största värden
Att hitta minsta och största värdena i ett BST är enkelt tack vare dess ordnade natur.
min() {
return this.minNode(this.root);
}
minNode(node) {
let current = node;
while (current !== null && current.left !== null) {
current = current.left;
}
return current;
}
max() {
return this.maxNode(this.root);
}
maxNode(node) {
let current = node;
while (current !== null && current.right !== null) {
current = current.right;
}
return current;
}
Exempel: Hitta minsta och största värden
console.log(bst.min().key); // Utskrift: 3
console.log(bst.max().key); // Utskrift: 25
Praktiska tillämpningar av binära sökträd
Binära sökträd används i en mängd olika tillämpningar, inklusive:
- Databaser: Indexering och sökning av data. Till exempel använder många databassystem variationer av BST, såsom B-träd, för att effektivt lokalisera poster. Tänk på den globala skalan av databaser som används av multinationella företag; effektiv datahämtning är av yttersta vikt.
- Kompilatorer: Symboltabeller, som lagrar information om variabler och funktioner.
- Operativsystem: Processplanering och minneshantering.
- Sökmotorer: Indexering av webbsidor och rangordning av sökresultat.
- Filsystem: Organisera och komma åt filer. Föreställ dig ett filsystem på en server som används globalt för att hosta webbplatser; en välorganiserad BST-baserad struktur hjälper till att servera innehåll snabbt.
Prestandaöverväganden
Prestandan hos ett BST beror på dess struktur. I bästa fall, ett balanserat BST, möjliggörs logaritmisk tidskomplexitet för insättnings-, sök- och borttagningsoperationer. Men i värsta fall (t.ex. ett skevt träd) kan tidskomplexiteten försämras till linjär tid.
Balanserade vs. obalanserade träd
Ett balanserat BST är ett där höjdskillnaden mellan vänster och höger underträd för varje nod är högst ett. Självbalanserande algoritmer, såsom AVL-träd och röd-svarta träd, säkerställer att trädet förblir balanserat och ger konsekvent prestanda. Olika regioner kan kräva olika optimeringsnivåer baserat på belastningen på servern; balansering hjälper till att upprätthålla prestanda under hög global användning.
Tidskomplexitet
- Insättning: O(log n) i genomsnitt, O(n) i värsta fall.
- Sökning: O(log n) i genomsnitt, O(n) i värsta fall.
- Borttagning: O(log n) i genomsnitt, O(n) i värsta fall.
- Genomgång: O(n), där n är antalet noder i trädet.
Avancerade BST-koncept
Självbalanserande träd
Självbalanserande träd är BST som automatiskt justerar sin struktur för att bibehålla balansen. Detta säkerställer att trädets höjd förblir logaritmisk, vilket ger konsekvent prestanda för alla operationer. Vanliga självbalanserande träd inkluderar AVL-träd och röd-svarta träd.
AVL-träd
AVL-träd upprätthåller balansen genom att säkerställa att höjdskillnaden mellan vänster och höger underträd för varje nod är högst ett. När denna balans störs utförs rotationer för att återställa balansen.
Röd-svarta träd
Röd-svarta träd använder färgegenskaper (röd eller svart) för att bibehålla balansen. De är mer komplexa än AVL-träd men erbjuder bättre prestanda i vissa scenarier.
JavaScript-kodexempel: Komplett implementering av binärt sökträd
class Node {
constructor(key) {
this.key = key;
this.left = null;
this.right = null;
}
}
class BinarySearchTree {
constructor() {
this.root = null;
}
insert(key) {
const newNode = new Node(key);
if (this.root === null) {
this.root = newNode;
} else {
this.insertNode(this.root, newNode);
}
}
insertNode(node, newNode) {
if (newNode.key < node.key) {
if (node.left === null) {
node.left = newNode;
} else {
this.insertNode(node.left, newNode);
}
} else {
if (node.right === null) {
node.right = newNode;
} else {
this.insertNode(node.right, newNode);
}
}
}
search(key) {
return this.searchNode(this.root, key);
}
searchNode(node, key) {
if (node === null) {
return false;
}
if (key < node.key) {
return this.searchNode(node.left, key);
} else if (key > node.key) {
return this.searchNode(node.right, key);
} else {
return true;
}
}
remove(key) {
this.root = this.removeNode(this.root, key);
}
removeNode(node, key) {
if (node === null) {
return null;
}
if (key < node.key) {
node.left = this.removeNode(node.left, key);
return node;
} else if (key > node.key) {
node.right = this.removeNode(node.right, key);
return node;
} else {
// nyckeln är lika med nodens nyckel
// fall 1 - en lövnod
if (node.left === null && node.right === null) {
node = null;
return node;
}
// fall 2 - noden har endast 1 barn
if (node.left === null) {
node = node.right;
return node;
} else if (node.right === null) {
node = node.left;
return node;
}
// fall 3 - noden har 2 barn
const aux = this.findMinNode(node.right);
node.key = aux.key;
node.right = this.removeNode(node.right, aux.key);
return node;
}
}
findMinNode(node) {
let current = node;
while (current != null && current.left != null) {
current = current.left;
}
return current;
}
min() {
return this.minNode(this.root);
}
minNode(node) {
let current = node;
while (current !== null && current.left !== null) {
current = current.left;
}
return current;
}
max() {
return this.maxNode(this.root);
}
maxNode(node) {
let current = node;
while (current !== null && current.right !== null) {
current = current.right;
}
return current;
}
inOrderTraverse(callback) {
this.inOrderTraverseNode(this.root, callback);
}
inOrderTraverseNode(node, callback) {
if (node !== null) {
this.inOrderTraverseNode(node.left, callback);
callback(node.key);
this.inOrderTraverseNode(node.right, callback);
}
}
preOrderTraverse(callback) {
this.preOrderTraverseNode(this.root, callback);
}
preOrderTraverseNode(node, callback) {
if (node !== null) {
callback(node.key);
this.preOrderTraverseNode(node.left, callback);
this.preOrderTraverseNode(node.right, callback);
}
}
postOrderTraverse(callback) {
this.postOrderTraverseNode(this.root, callback);
}
postOrderTraverseNode(node, callback) {
if (node !== null) {
this.postOrderTraverseNode(node.left, callback);
this.postOrderTraverseNode(node.right, callback);
callback(node.key);
}
}
}
// Exempel på användning
const bst = new BinarySearchTree();
bst.insert(11);
bst.insert(7);
bst.insert(15);
bst.insert(5);
bst.insert(3);
bst.insert(9);
bst.insert(8);
bst.insert(10);
bst.insert(13);
bst.insert(12);
bst.insert(14);
bst.insert(20);
bst.insert(18);
bst.insert(25);
const printNode = (value) => console.log(value);
console.log("In-order-genomgång:");
bst.inOrderTraverse(printNode);
console.log("Pre-order-genomgång:");
bst.preOrderTraverse(printNode);
console.log("Post-order-genomgång:");
bst.postOrderTraverse(printNode);
console.log("Minsta värde:", bst.min().key);
console.log("Största värde:", bst.max().key);
console.log("Sök efter 9:", bst.search(9));
console.log("Sök efter 2:", bst.search(2));
bst.remove(7);
console.log("Sök efter 7 efter borttagning:", bst.search(7));
Slutsats
Binära sökträd är en kraftfull och mångsidig datastruktur med många tillämpningar. Denna guide har gett en omfattande översikt över BST, som täcker deras struktur, operationer och implementering i JavaScript. Genom att förstå principerna och teknikerna som diskuteras i denna guide kan utvecklare världen över effektivt använda BST för att lösa ett brett spektrum av problem inom mjukvaruutveckling. Från att hantera globala databaser till att optimera sökalgoritmer är kunskapen om BST en ovärderlig tillgång för alla programmerare.
När du fortsätter din resa inom datavetenskap kommer utforskandet av avancerade koncept som självbalanserande träd och deras olika implementeringar att ytterligare förbättra din förståelse och dina färdigheter. Fortsätt att öva och experimentera med olika scenarier för att bemästra konsten att använda binära sökträd effektivt.